閱讀本篇文章前,今天什麼都不用想!
直接進入正文,快看下面!
筆者就直接讓油門繼續摧下去~正文開始!
筆者 O.S.:今天又是數學時間,要學好程式可真不簡單,但學好數學可以應用在程式上,何嘗不可?
基本上,筆者發現有些在學 TypeScript union
與 intersection
的過程,有人會以為,或者是有這樣的誤解:
TypeScriptunion
與intersection
跟數學上對於集合的聯集與交集的定義是一樣的
很抱歉,以上那句話 —— 大・錯・特・錯 —— 所以被筆者狠狠地劃上了很大的刪節線!
筆者有點想把取這個很讓人誤解的名詞的人給推入火坑。
以下就是踢爆這個誤區的推論時間!
數學定義上的聯集(Union)與交集(Intersection)的概念,通常會有簡單的 Venn Diagram 來展示,我們有集合 A 與 B 呈現如圖一:
圖一:集合 A 的範圍為左方橘色圈圈部分;集合 B 的範圍為右方藍色圈圈部分A 聯集 B(又稱 A 與 B 的 Union)為 A 與 B 包含在一起的範圍 —— 其中,聯集過後元素不可重複,這是集合本身的特性。
而 A 交集 B(又稱 A 與 B 的 Intersection)為 A 與 B 重疊的範圍。
假設 A 集合包含以下元素:
{ 1, 2, 3, 4 }
假設 B 集合包含以下元素:{ 2, 4, 6, 8 }
其中 A 聯集 B 的集合結果為:
{ 1, 2, 3, 4, 6, 8 }
而 A 交集 B 的集合結果為:{ 2, 4 }
那以上的定義跟 TypeScript 的複合型別有什麼差別?
首先筆者先從最簡單的方向開始,也就是我們常看到的聯集 union
:
如果按照數學的概念推理的話,A 為 number
與 string
的聯集,也就是說 UnionSet1
可以是 number
或 string
。不過筆者再舉下個例子:
讀者可能覺得:“作者是想表達什麼?這麼簡單的事情:UnionSet2
可以是 UserInfo1
或是 UserInfo2
啊,因為 UserInfo1
與 UserInfo2
都是屬於 UnionSet2
的範疇。”
好,請注意這句話:
UnionSet2
可以是UserInfo1
或是UserInfo2
那根據數學推理,照理來說應該只會有這三種組合:
以上就留給讀者進行驗證,TypeScript 檢測結果不會出錯。
好的,那麼以下這些結果,根據強制性的數學推理:就算某變數被註記為 UnionSet2
,而該變數裡的值已經完全滿足 UserInfo1
或 UserInfo2
其中一種型別,若另一個型別若完全不滿足的話,理應來說 TypeScript 要發出錯誤警訊。(以下程式碼檢測結果如圖二)
圖二:理所當然,第一個案例一定錯,因為既不滿足 UserInfo1
也不滿足 UserInfo2
;然而後續的例子,只要至少其中一個型別被判定滿足,不管其他型別有沒有完整補齊,TypeScript 認為無所謂
筆者舉一個更諷刺的例子:空集合。(Empty Set)
讀者一看就知道這一定會出錯(讀者可以自行驗證),因為什麼都不滿足。
然而,筆者必須請讀者回憶一下:“集合論裡,空集合不也是屬於任何聯集過後的集合中的元素嗎?” —— 這個機制就很像原生 JS 裡還是必須要有代表空值的 undefined
或代表空這個概念的值的 null
。(當然啦,總是會有開發者閒 null
跟 undefined
這兩種東西比較起來還是很蠢,不過這裡筆者沒有什麼太大的異義)
藉由以上的試驗,TypeScript 的 union
型別完全不符合數學理論所預期的規則。綜觀 TypeScript 已經出現很久了,使用複合型別的過程中,開發者們在認為理所當然的應用情境下違反了以前學過的最基礎的數學原理,甚至也沒意識到這樣的嚴重性,造成錯誤的觀念亂散播出去。(一開始就對數學家的專業不尊重,遑論尊不尊重軟體開發者的專業)
再來想一下另一種案例,數學的交集跟 TypeScript 的 intersection
有沒有差別。
然而筆者不得不說,這裡數學定義的交集又跟 TS 的 intersection
完全背道而馳!請讀者想想看,以下的案例:
根據 UserInfo1
跟 UserInfo2
各自的型別格式,有沒有認真想過它們有何交集點?
name
與 age
以及 hasPet
與 ownsMotorcycle
的組合 —— 兩組屬性的集合中,完全沒有交集!
偏偏在 TypeScript 裡 —— 交集 intersection
的用法是:將兩個可以為型別或介面的組合裡的格式進行結合的概念。
也就是說,如果我們註記 IntersectionSet
在某變數 B
上,該變數 B
必須實踐出 name
、age
、hasPet
跟 ownsMotorcycle
這四種屬性!否則會出錯呢。(以下程式碼檢驗結果如圖三)
圖三:屬性缺一不可啊!
在這裡,筆者必須向讀者澄清,這怎麼能說是交集呢?
筆者認為,我們應該換個想法 —— TypeScript 的 union
與 intersection
的原理沒有跟數學的集合論符合,但倒是符合另一個模型!讀者想得到嗎?
布林代數的邏輯(Boolean Logic)!
學了那麼久的 And
與 Or
邏輯,應該很明顯的:|
是我們常看到的 OR 的概念;&
則是我們常看到的 AND 的概念。
想想看,剛剛使用 union
時的情境:
將
UserInfo1
跟UserInfo2
進行union
-- 把它轉換成:將UserInfo1
和UserInfo2
OR 起來之後是不是邏輯通順很多?
- 你可以選擇只要符合
UserInfo1
要求的型別或介面格式- 或(OR)你可以選擇只要符合
UserInfo2
要求的型別或介面格式- 但你也可以全部都符合
- **不過就是至少一個條件一定要滿足,否則出錯!**因此空集合概念在這裡也不覆存在,會被認定是錯的,因為 OR 邏輯本來就是建立在其中一方符合的條件下才能滿足的。
對比使用 intersection
時:
將
UserInfo1
跟UserInfo2
進行intersection
-- 把它轉換成:將UserInfo1
與UserInfo2
AND 起來之後是不是邏輯通順很多?
- 你必須符合
UserInfo1
和(AND)UserInfo2
的型別或介面格式- 只要少了一個屬性就會出錯,完全符合 AND 邏輯的真諦
當初取 union
跟 intersection
這兩個名稱的研發者,不是數學概念不好,就是亂用取錯名,簡直是誤導群眾、誤導開發者。
重點 1. 複合型別的基礎法則 Fundamental Law of TS Intersection & Union
複合型別(
intersection
與union
)在 TypeScript 的運作邏輯完全不等於數學裡集合論的定義。相對地,複合型別的概念反而是跟布林邏輯的概念符合!
重點 1. 都已經用『 法則 』這兩個字形容了,應該可以提醒讀者這個概念的重要性了吧。
重點 2. 複合型別的語法與規則
假設某型別化名
A
與B
,其中A
與B
各自可以為型別或者是介面,亦或者都是型別或介面,其中TUnion
為A
與B
的union
;而TIntersection
為A
與B
的intersection
,則:type TUnion = A | B; type TIntersection = A & B;
任意變數
C
,其中C
被註記為TUnion
型別,則C
的值必須至少符合A
或B
其中一項型別的完整靜態格式(或實踐出其中一個介面裡的所有功能,如果A
或B
有存在介面宣告的話)。任意變數
D
,其中D
被註記為TIntersection
型別,則D
的值必須完全符合A
與B
裡所有的型別靜態格式(或實踐出其中一個介面裡的所有功能,如果A
或B
有存在介面宣告的話)。
接下來就要看一些使用 union
與 intersection
會發生的莫名有趣現象。
讀者可能以為 union
與 intersection
就只是剛剛講的就結束了~
沒有喔,還有很多可以講 XD,因此筆者才會放在很後面。
我們剛剛有展示過,原始型別的複合 -- 對 A
或 B
型別使用 union
,其中 A
不等於 B
且 A
和 B
皆屬於原始型別。
我們都很熟悉 A
與 B
進行 union
,然而我們可曾想過將這 A
與 B
型別作 intersection
的結果?
試想一下:number & string
到底是什麼?其實讀者如果有把本系列文章讀熟一定會有答案呢!
筆者突然浮現出的 Brainstorming 過程:
“恩 ...... 要能夠同時為
number
以及string
... 難道不是空集合嗎?”“可是空集合在 TypeScript 作者好像沒講到啊 ...”
“世界上真的有既是數字和字串的東西嗎?還是說將數字加兩個引號就可以了?就像這樣:'42'
”
筆者回答:當然不存在,但是隱身於所有型別當中的共通點 —— 以下這句話節選自 Day 10. Never 型別:
never
is a subtype of and assignable to every type.
哦~其實這也挺合理的:“既然是不存在的型別交集,最後的結論理應是:例外狀況或不可能的狀況出現,因此推得兩個原始型別的交集結果是 never
型別”。(印證的結果如圖四)
圖四:原始型別交集結果就是 never
讀者是不是覺得 Never 型別存在的意義 比想像中重要?筆者認為其他的教學資源基本上只是幾百字不到淺淺帶過 never
型別的語法與用途 —— 但從來沒有把 never
的真諦傳出去,實是可惜,不然這些特殊型別的行為也挺有趣的。
重點 3. 原始型別的交集
若
TA
與TB
皆為原始型別,且TA
與TB
不相同,則兩型別被intersection
的結果為never
型別:type MustBeNever = TA & TB;
因此,
MustBeNever
為never
型別
讀者試試看
如果將廣義物件型別跟原始型別進行
intersection
:
- 結果判定是
never
嗎?- 如果不是的話,那效果上跟
never
差不多嗎?
不過讀者仔細想想,這不就跟 Never 型別那一篇 ,後面的 intersection
提到的概念很像嗎?
《 Day 10. 特殊型別 X 永無止盡 - Never Type 》之 重點 2.
never
型別為所有型別的 Subtype任何型別 T(包含
never
本身)和never
進行union
,則型別 T 會吸收掉never
型別:type WontBeNever = T | never; // => WontBeNever: T
任何型別 U(包含
never
本身)和never
進行intersection
,則型別 U 會被never
型別強行覆蓋:type MustBeNever = U & never; // => MustBeNever: never
其實本篇的重點 3. 不過就是 Never 型別篇章 重點 2. 的擴充呢!
其實筆者真的覺得中文好難翻,甚至也懶得去查 —— 寫作到這裡偶然查到原來 Generics 的翻譯為泛用型別並不是通用型別。(筆者掩面感到丟臉)
因此,可能會將文章裡的使用詞再進行修改。不過 Type Guard 筆者也很難找到翻譯,只知道可以被形容成型別限縮的概念。
後來覺得『 型別檢測 』這名詞好像也不錯,乾脆就採用這個名詞 —— 作用依然還是跟型別的限縮有關!
讀者如果看過本系列,這個技巧應該在 Any v.s. Unknown 型別篇章 遇過一次,那時候是在討論 —— 如果變數被註記或推論為 unknown
型別時,該變數基本上什麼事情都不能做,這裡原封不動貼上當時的重點:
《Day 11. 特殊型別 X 無法無天 - Any & Unknown Type 》之 重點 2. unknown 型別下的變數指派限制
- 跟
any
型別相似的地方在於,若變數被unknown
型別註記,則該變數可以被任意型別的值指派- 若被註記為
unknown
型別的變數,除了以下情形以外,否則不得將其值指派到任意型別(除了unknown
或any
)的變數裡:
- 顯性註記之型別
T
等同於被指派到的變數之型別T
- 根據程式的控制流程分析,其
unknown
型別的推論被*限縮到特定的型別U
*致使可以被指派到其他符合型別 U 條件的變數
其中第二點的最後一句話:
“根據程式的控制流程分析,其
unknown
型別的推論被限縮到特定的型別U
致使可以被指派到其他符合型別U
條件的變數”
關鍵字是當時筆者說的兩個點(不是人體上的兩個點,筆者的有些朋友會這樣亂想,但這是公眾場合XD):“控制流程分析”與“推論被限縮”。
這很明顯在說明 Type Guard 的重點 —— 藉由簡單的判斷敘述來限縮型別的技巧,因此當時候才會有這個範例:
不過筆者這裡不在多做以上程式碼範例的說明,認為需要再複習 unknown
型別的概念請參考 Day 11.。
那筆者為何到現在才講 Type Guard?
通常碰到 union
過後的型別,多數狀況下我們必須主動使用 Type Guard 讓 TypeScript 編譯器不會哀哀叫。其實之前在 介面的函式超載篇章 裡的例子舉得不錯,我們重新來看:
當時 AddOperation
的介面長這樣:
其中我們遇到的狀況是,要實踐 AddOperation
難免會遇到 union
的狀況,該函式的參數 p1
與 p2
各自可為 string
或 number
,因此裡面才會需要 if...else...
判斷式進行參數型別的判斷。
我們今天來看看另一種狀況。
以上這個 ISummation
介面是屬於純粹函式格式的介面,也引用了函式超載的概念。
這裡想要達到的效果是 -- 假設某函式 F
已經實踐了 ISummation
所訂立的功能規格,則:
其中,若讀者不熟悉 ...args
這種在函式參數裡面的行為(這個叫做匯聚操作子 Rest-Operator),請多參考社群們熱心教學的 ES6 系列文章 ~
那麼以下筆者就不客氣地丟出實踐結果。
筆者這一次是第四次編譯本並且測試結果。讀者如果曉得流程,應該也會直接果斷下 tsc
然後再去用 node
執行編譯出來的 index.js
。(結果如圖五)
圖五:驗證結果為正確
貼心小提示
有些讀者認為,檢測陣列可以用
Array.isArray
,這一點也是沒問題的!不過記得要在tsconfig.json
裡進行微調:{ "compilerOptions": { /* 略... */ "lib": ["dom", "es2015"], /* 略... */ } }
有關於編譯器設定將會在《戰線擴張》系列進行介紹,筆者已經確定那時候是 30 天以後囉~
這裡筆者想強調的重點是 —— 通常檢測原始型別都是為用 typeof
這個關鍵字。
`typeof value === ''
而通常廣義物件或類別(Class)建造出來的物件則是會用 instanceof
這個關鍵字:
someObject instanceof ObjectBelongingClass
最常遇到的應該是諸如此類的問題:如何寫限縮型別的判斷式。
重點 4. 限縮型別的技巧 - 型別檢測 Type Guard
- 若想要過濾出純原始型別的值的話,使用
typeof
操作子- 若想要過濾出廣義物件型別的值的話,使用
instanceof
判斷操作子,並填上屬於該物件型別所屬的類別- 其他方式,譬如
Array.isArray
可以檢測陣列
今天總算了結複合型別,筆者實在是感到溫馨。(讀者看到篇幅大小感到煩躁)
筆者接下來要進行的是 TypeScript Class 也就是類別部分的介紹啦~
筆者這邊再次強調:你不需要懂 ES6 Class,筆者幫你建立這方面的基礎,因為基本上學完 TypeScript Class 就等於你會了 ES6 Class 大部分的內容~
而類別的部分也是為了鋪陳本系列後續的重頭戲必備的知識呀!
感謝大大分享XD
最近剛好看到union 以及 intersection的地方
想說怎麼跟我原先的認知不同
如果從語法上的 & 以及 | 來看
用邏輯布林去理解時的確清楚許多了~
對,基本上 union 跟 intersection 我認為是當初訂規則的人對數學有某種程度誤用,這個關係應該用布林邏輯表示會很適合唷
文章好像有錯誤:
「任意變數 D,其中 D 被註記為 TIntersection 型別,則 D 的值必須完全符合 A 與 B 裡所有的型別靜態格式(或實踐出其中一個介面裡的所有功能,如果 A 或 B 有存在介面宣告的話)。」
上面小括號內的描述不太對, 應該要實踐所有介面裡的所有功能...。這小括號的內容完全跟它的前一段內容的小括號一模一樣, 而前一段是在描述Union, 我想筆者原本應該是想直接把文字複製過來再修改其中幾個字, 但忘了修改。